package shun.demo.remote.display.client;

import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.*;
import android.widget.FrameLayout;
import android.widget.Scroller;
import shun.demo.remote.display.model.Movement;

/**
 * Created by chen on 14-5-21.
 */
public class MapView extends FrameLayout {

    private static final int INVALID_POINTER = -1;
    private static final String TAG = "MapView";
    private boolean mIsBeingDragged;
    private int mActivePointerId;
    private int mLastMotionX;
    private int mLastMotionY;
    private int mTouchSlop;
    private int mMinimumVelocity;
    private int mMaximumVelocity;
    private VelocityTracker mVelocityTracker;
    private float[] mTmpTransformLocation = new float[2];
    private View mTouchedChild;
    private Scroller mScroller;
    private ObjectAnimator mAnimatorX;
    private ObjectAnimator mAnimatorY;


    public MapView(Context context) {
        super(context);
        init();
    }

    public MapView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MapView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
        mTouchSlop = configuration.getScaledTouchSlop();
        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
        mScroller = new Scroller(getContext());
    }

    private void initVelocityTrackerIfNotExists() {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    private void recycleVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    private void initOrResetVelocityTracker() {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        } else {
            mVelocityTracker.clear();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        /*
         * This method JUST determines whether we want to intercept the motion.
         * If we return true, onMotionEvent will be called and we do the actual
         * scrolling there.
         */

        /*
        * Shortcut the most recurring case: the user is in the dragging
        * state and he is moving his finger.  We want to intercept this
        * motion.
        */
        final int action = ev.getAction();
        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
            return true;
        }

        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_MOVE: {
                /*
                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
                 * whether the user has moved far enough from his original down touch.
                 */

                /*
                * Locally do absolute value. mLastMotionY is set to the y value
                * of the down event.
                */
                final int activePointerId = mActivePointerId;
                if (activePointerId == INVALID_POINTER) {
                    // If we don't have a valid id, the touch down wasn't on content.
                    break;
                }

                final int pointerIndex = ev.findPointerIndex(activePointerId);
                if (pointerIndex == -1) {
                    Log.e(TAG, "Invalid pointerId=" + activePointerId
                            + " in onInterceptTouchEvent");
                    break;
                }

                final int x = (int) ev.getX(pointerIndex);
                final int y = (int) ev.getY(pointerIndex);
                final int xDiff = Math.abs(x - mLastMotionX);
                final int yDiff = Math.abs(y - mLastMotionY);
                if (xDiff > mTouchSlop || yDiff > mTouchSlop) {
                    mIsBeingDragged = true;
                    mLastMotionY = y;
                    mLastMotionX = x;
                    initVelocityTrackerIfNotExists();
                    mVelocityTracker.addMovement(ev);
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                }
                break;
            }

            case MotionEvent.ACTION_DOWN: {
                final int x = (int) ev.getX();
                final int y = (int) ev.getY();
                mActivePointerId = ev.getPointerId(0);
                mTouchedChild = getTouchedChild(ev);
                if (mTouchedChild != null) {
                    mIsBeingDragged = false;
                    recycleVelocityTracker();
                    break;
                }

                /*
                 * Remember location of down touch.
                 * ACTION_DOWN always refers to pointer index 0.
                 */
                mLastMotionX = x;
                mLastMotionY = y;

                initOrResetVelocityTracker();
                mVelocityTracker.addMovement(ev);
                /*
                * If being flinged and user touches the screen, initiate drag;
                * otherwise don't.  mScroller.isFinished should be false when
                * being flinged.
                */
//                mIsBeingDragged = !mScroller.isFinished();
                break;
            }

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                /* Release the drag */
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                recycleVelocityTracker();
//                if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
//                    postInvalidateOnAnimation();
//                }
                break;
            case MotionEvent.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                break;
        }

        /*
        * The only time we want to intercept motion events is if we are in the
        * drag mode.
        */
        return mIsBeingDragged;
    }

    private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
        final int pointerId = ev.getPointerId(pointerIndex);
        if (pointerId == mActivePointerId) {
            // This was our active pointer going up. Choose a new
            // active pointer and adjust accordingly.
            // TODO: Make this decision more intelligent.
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            mLastMotionY = (int) ev.getY(newPointerIndex);
            mActivePointerId = ev.getPointerId(newPointerIndex);
            if (mVelocityTracker != null) {
                mVelocityTracker.clear();
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        initVelocityTrackerIfNotExists();
        mVelocityTracker.addMovement(ev);

        final int action = ev.getAction();

        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN: {
                if (getChildCount() == 0) {
                    return false;
                }
//                if ((mIsBeingDragged = !mScroller.isFinished())) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
//                }

                /*
                 * If being flinged and user touches, stop the fling. isFinished
                 * will be false if being flinged.
                 */
//                if (!mScroller.isFinished()) {
//                    mScroller.abortAnimation();
//                    if (mFlingStrictSpan != null) {
//                        mFlingStrictSpan.finish();
//                        mFlingStrictSpan = null;
//                    }
//                }

                // Remember which child the user touched
                mTouchedChild = getTouchedChild(ev);

                // Remember where the motion event started
                mLastMotionX = (int) ev.getX();
                mLastMotionY = (int) ev.getY();
                mActivePointerId = ev.getPointerId(0);
                break;
            }
            case MotionEvent.ACTION_MOVE:
                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                if (activePointerIndex == -1) {
                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
                    break;
                }

                final int x = (int) ev.getX(activePointerIndex);
                final int y = (int) ev.getY(activePointerIndex);
                int deltaX = mLastMotionX - x;
                int deltaY = mLastMotionY - y;
                if (!mIsBeingDragged && (Math.abs(deltaX) > mTouchSlop || Math.abs(deltaY) > mTouchSlop)) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                    mIsBeingDragged = true;
                    if (Math.abs(deltaX) > mTouchSlop) {
                        if (deltaX > 0) {
                            deltaX -= mTouchSlop;
                        } else {
                            deltaX += mTouchSlop;
                        }
                    }
                    if (Math.abs(deltaY) > mTouchSlop) {
                        if (deltaY > 0) {
                            deltaY -= mTouchSlop;
                        } else {
                            deltaY += mTouchSlop;
                        }
                    }
                }
                if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    mLastMotionX = x;
                    mLastMotionY = y;

                    if (mTouchedChild != null) {
                        mTouchedChild.setX(mTouchedChild.getX() - deltaX);
                        mTouchedChild.setY(mTouchedChild.getY() - deltaY);
                    }
//                    final int oldX = mScrollX;
//                    final int oldY = mScrollY;
//                    final int range = getScrollRange();
//                    final int overscrollMode = getOverScrollMode();
//                    final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
//                            (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
//
//                    // Calling overScrollBy will call onOverScrolled, which
//                    // calls onScrollChanged if applicable.
//                    if (overScrollBy(0, deltaY, 0, mScrollY,
//                            0, range, 0, mOverscrollDistance, true)) {
//                        // Break our velocity if we hit a scroll barrier.
//                        mVelocityTracker.clear();
//                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                if (mIsBeingDragged) {
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    int xVelocity = (int) velocityTracker.getXVelocity(mActivePointerId);
                    int yVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
                    if (mTouchedChild != null) {
                        mScroller.fling((int) mTouchedChild.getX(), (int) mTouchedChild.getY(), xVelocity, yVelocity,
                                Integer.MIN_VALUE, Integer.MAX_VALUE,
                                Integer.MIN_VALUE, Integer.MAX_VALUE);
                        Log.v(XScreen.LOG_TAG, "final x: " + mScroller.getFinalX());
                        Log.v(XScreen.LOG_TAG, "final y: " + mScroller.getFinalY());
                        Log.v(XScreen.LOG_TAG, "duration: " + mScroller.getDuration());
                        int startX = mScroller.getStartX();
                        int startY = mScroller.getStartY();
                        int finalX = mScroller.getFinalX();
                        int finalY = mScroller.getFinalY();
                        long duration = mScroller.getDuration();
                        Movement movement = new Movement(startX, finalX, startY, finalY,
                                duration, Movement.DECELERATE_INTERPOLATOR);
                        XScreen.get().send(movement);
                        mAnimatorX = movement.createAnimatorXFor(mTouchedChild);
                        mAnimatorY = movement.createAnimatorYFor(mTouchedChild);
                        mAnimatorX.start();
                        mAnimatorY.start();
                    }

                    mActivePointerId = INVALID_POINTER;
                    endDrag();
                } else {
                    // TODO: perform click, now move the children to this point
                    View child = getChildAt(0);

                    Movement movement = new Movement(child.getX(), mLastMotionX + getScrollX(),
                            child.getY(), mLastMotionY + getScrollY(), 500);
                    XScreen.get().send(movement);
                    mAnimatorX = movement.createAnimatorXFor(child);
                    mAnimatorY = movement.createAnimatorYFor(child);
                    mAnimatorX.start();
                    mAnimatorY.start();
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                if (mIsBeingDragged && getChildCount() > 0) {
//                    if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
//                        postInvalidateOnAnimation();
//                    }
                    mActivePointerId = INVALID_POINTER;
                    endDrag();
                }
                break;
            case MotionEvent.ACTION_POINTER_DOWN: {
                final int index = ev.getActionIndex();
                mLastMotionY = (int) ev.getY(index);
                mActivePointerId = ev.getPointerId(index);
                break;
            }
            case MotionEvent.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
                break;
        }
        return true;
    }

    private void endDrag() {
        mIsBeingDragged = false;

        recycleVelocityTracker();

    }

    public void fling(int velocityY) {
//        if (getChildCount() > 0) {
//            int height = getHeight() - mPaddingBottom - mPaddingTop;
//            int bottom = getChildAt(0).getHeight();
//
//            mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,
//                    Math.max(0, bottom - height), 0, height/2);
//
//            postInvalidateOnAnimation();
//        }
    }

    private boolean inChild(int x, int y) {
        if (getChildCount() > 0) {
            final int scrollY = getScrollY();
            final View child = getChildAt(0);
            return !(y < child.getTop() - scrollY
                    || y >= child.getBottom() - scrollY
                    || x < child.getLeft()
                    || x >= child.getRight());
        }
        return false;
    }

    private View getTouchedChild(MotionEvent ev) {
        final int actionIndex = ev.getActionIndex();
        final int childrenCount = getChildCount();
        if (childrenCount != 0) {
            final float x = ev.getX(actionIndex);
            final float y = ev.getY(actionIndex);
            // Find a child that can receive the event.
            // Scan children from front to back.

            final boolean customOrder = isChildrenDrawingOrderEnabled();
            for (int i = childrenCount - 1; i >= 0; i--) {
                final int childIndex = customOrder ?
                        getChildDrawingOrder(childrenCount, i) : i;
                final View child = getChildAt(childIndex);
                if (!canViewReceivePointerEvents(child)
                        || !isTransformedTouchPointInView(x, y, child, null)) {
                    continue;
                }

                return child;
            }
        }
        return null;
    }

    private static boolean canViewReceivePointerEvents(View child) {
        return child.getVisibility() == VISIBLE
                || child.getAnimation() != null;
    }

    protected boolean isTransformedTouchPointInView(float x, float y, View child,
                                                    PointF outLocalPoint) {
        float localX = x + getScrollX() - child.getLeft();
        float localY = y + getScrollY() - child.getTop();
        if (! child.getMatrix().isIdentity() && child.getWindowVisibility() == VISIBLE) {
            final float[] localXY = mTmpTransformLocation;
            localXY[0] = localX;
            localXY[1] = localY;
            getInverseMatrix(child).mapPoints(localXY);
            localX = localXY[0];
            localY = localXY[1];
        }
        final boolean isInView = pointInView(child, localX, localY);
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(localX, localY);
        }
        return isInView;
    }

    public static boolean pointInView(View view, float localX, float localY) {
        return localX >= 0 && localX < (view.getWidth())
                && localY >= 0 && localY < (view.getHeight());
    }

    public static Matrix getInverseMatrix(View view) {
        Matrix matrix = view.getMatrix();
        if (!matrix.isIdentity()) {
            Matrix inverseMatrix = new Matrix();
            matrix.invert(inverseMatrix);
            return inverseMatrix;
        }
        return matrix;
    }
}
